package com.mezz.bones.framework.web.filter;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.ImmutableSet;
import com.mezz.bones.framework.base.context.RequestContextHolder;
import com.mezz.bones.framework.web.domain.RequestLog;
import com.mezz.bones.framework.web.handler.log.RequestLogStoreHandler;
import com.mezz.bones.framework.web.handler.login.info.RequestLoginUserInfoHandler;
import com.mezz.bones.framework.web.service.ApiStatisticsService;
import com.mezz.bones.framework.web.servlet.RepeatableHttpServletRequestWrapper;
import com.mezz.bones.framework.web.servlet.RepeatableHttpServletResponseWrapper;
import com.mezz.bones.framework.web.util.FilterUtils;
import com.mezz.bones.framework.web.util.Ip2RegionUtils;
import com.mezz.bones.framework.web.util.ServletUtils;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Part;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.support.MultipartResolutionDelegate;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Set;
import java.util.stream.Collectors;

@Component
@Slf4j
public class ApmHttpInfoFilter implements Filter {

    /**
     * 应用名称
     */
    @Value("${spring.application.name}")
    private String applicationName;

    private static final ImmutableSet<String> IGNORED_HEADERS;

    @Resource
    private RequestLogStoreHandler requestLogStoreHandler;

    @Resource
    private RequestLoginUserInfoHandler requestLoginUserInfoHandler;

    @Resource
    private ApiStatisticsService apiStatisticsService;

    //某些响应数据过大 可截取部分数据
    private static final Set<String> outIgnoredUrl = ImmutableSet.of(
            "/sysProtocolContent/list");

    static {
        Set<String> ignoredHeaders = ImmutableSet.of(
                        "User-Agent",
                        "Accept",
                        "Cache-Control",
                        "Postman-Token",
                        "Host",
                        "sec-ch-ua-platform",
                        "sec-ch-ua-mobile",
                        "sec-fetch-site",
                        "sec-fetch-mode",
                        "sec-fetch-dest",
                        "accept-language",
                        "cookie",
                        "Accept-Encoding",
                        "Connection",
                        "authorization",
                        "sec-ch-ua",
                        "Brand",
                        "Content-Length")
                .stream()
                .map(String::toUpperCase)
                .collect(Collectors.toSet());
        IGNORED_HEADERS = ImmutableSet.copyOf(ignoredHeaders);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {


        Long startTime = System.currentTimeMillis();

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        RepeatableHttpServletRequestWrapper requestWrapper = new RepeatableHttpServletRequestWrapper(request);
        RepeatableHttpServletResponseWrapper responseWrapper = new RepeatableHttpServletResponseWrapper(response);

        RequestLog requestLog = new RequestLog();

        try {

            //如果是文件流则不记录日志信息
            if (isMultipartFileRequest(request)) {
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            }

            String input = getInputParams(requestWrapper);
            requestLog.setReqInput(input);

            //应用名称
            requestLog.setApplicationName(applicationName);

            //当前登录的用户ID
            String loginId = getLoginId(requestWrapper);
            requestLog.setLoginId(loginId);

            //请求版本号(APP端)
            String version = ServletUtils.getVersion();
            requestLog.setVersion(version);

            //请求客户端信息
            String userAgent = ServletUtils.getUserAgent();
            requestLog.setUserAgent(userAgent);

            //请求设备
            String device = ServletUtils.getDevice();
            requestLog.setDevice(device);

            //请求路径
            String reqUrl = requestWrapper.getRequestURI();
            requestLog.setReqUrl(reqUrl);

            //请求方法
            String method = requestWrapper.getMethod();
            requestLog.setReqMethod(method);

            //请求时间
            String createTime = DateUtil.now();
            requestLog.setCreateTime(createTime);

            //客户端IP
            String ip = ServletUtils.fetchIp();
            requestLog.setIp(ip);

            //IP归属地 region
            String region = Ip2RegionUtils.ip2Region(ip);
            requestLog.setRegion(region);

            //记录请求上下文
            doSaveReqContext(requestLog);
            filterChain.doFilter(requestWrapper, responseWrapper);

        } catch (Exception e) {

            //记录异常信息
            requestLog.setExceptionInfo(e.getMessage());
            FilterUtils.filterHandlerException(requestLog, responseWrapper, e);

        } finally {
            try {
                Long endTime = System.currentTimeMillis();
                Long execTime = endTime - startTime;
                //执行时间
                requestLog.setExecTime(execTime);

                String outputResponse = getOutputResponse(request, responseWrapper);
                requestLog.setReqOutPut(outputResponse);

                //存储日志 可自定义存储方式
                requestLogStoreHandler.processLog(requestLog);

                //清楚请求上下文
                RequestContextHolder.clear();

                //简单做个流量统计
                apiStatisticsService.statistics(requestLog);
            }catch (Exception ignored){
                log.error("ApmHttpInfoFilter finally error ",ignored);
            }
        }
    }

    private void doSaveReqContext(RequestLog requestLog) {

        RequestContextHolder.RequestContext context = new RequestContextHolder.RequestContext();

        context.setLoginId(requestLog.getLoginId());
        context.setVersion(requestLog.getVersion());
        context.setUserAgent(requestLog.getUserAgent());
        context.setDevice(requestLog.getDevice());
        context.setReqUrl(requestLog.getReqUrl());
        context.setIp(requestLog.getIp());
        context.setTenantId(requestLog.getTenantId());

        RequestContextHolder.RequestLoginUserInfo loginUserInfo = requestLoginUserInfoHandler.getLoginUserInfo(requestLog.getLoginId());
        context.setLoginUserInfo(loginUserInfo);

        RequestContextHolder.set(context);

    }

    /**
     * 判断请求体是否携带文件
     *
     * @param request 请求体
     */
    private boolean isMultipartFileRequest(HttpServletRequest request) throws ServletException, IOException {
        // 判断请求类型是否为 multipart/form-data
        boolean isMultipart = MultipartResolutionDelegate.isMultipartRequest(request);

        if (isMultipart) {
            // getParts 会缓存请求参数到 HttpServletRequest 中
            // 控制层可正常获取参数
            Collection<Part> parts = request.getParts();
            for (Part part : parts) {
                // 通过是否上传文件名判断参数是否为文件类型
                if (part.getSubmittedFileName() != null) {
                    return true;
                }
            }
        }
        return false;
    }

    private String getLoginId(RepeatableHttpServletRequestWrapper request) {

        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            if (headerName.contains("token")) {
                String token = request.getHeader(headerName);
                if (StrUtil.isNotBlank(token)) {
                    //解析token当中的uid
                    Object loginId = StpUtil.getLoginIdByToken(token);
                    if (loginId != null) {
                        return loginId.toString();
                    }
                }
            }
        }

        return "";
    }

    private String getOutputResponse(HttpServletRequest request, RepeatableHttpServletResponseWrapper responseWrapper) {
        //获取返回值body
        String responseBody = responseWrapper.getContent();
        //输出到output
        if (outIgnoredUrl.contains(request.getRequestURI())) {
            responseBody = StrUtil.sub(responseBody, 0, 1024);
        }
        return responseBody;
    }

    private String getInputParams(RepeatableHttpServletRequestWrapper request) {
        //构造请求信息: 比如 curl -X GET http://localhost:18080/getPerson?id=1 -H 'token: me-token' -d '{ "name": "hello" }'
        //构造请求的方法&URL&参数
        StringBuilder sb = new StringBuilder("curl")
                .append(" -X ").append(request.getMethod())
                .append(" ").append(request.getRequestURL().toString());
        if (StringUtils.hasLength(request.getQueryString())) {
            // linux中url请求参数的&需要做处理,否则无法进行调用
            String newQueryString = request.getQueryString().replace("&", "\\&");
            sb.append("?").append(newQueryString);
        }

        //构造header
        Enumeration<String> headerNames = request.getHeaderNames();
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            if (!IGNORED_HEADERS.contains(headerName.toUpperCase())) {
                sb.append(" -H '").append(headerName).append(": ").append(request.getHeader(headerName)).append("'");
            }
        }

        // 若是表单请求类型,已经处理过 可直接拼接
        if (StrUtil.containsAnyIgnoreCase(request.getContentType(), "application/x-www-form-urlencoded", "form-data")) {
            sb.append(request.getBody());
            request.setBody("");
        }

        //获取body
        String body = request.getBody();
        if (StringUtils.hasLength(body)) {
            sb.append(" -d '").append(body).append("'");
        }
        return sb.toString();
    }
}
